package org.sqlproc.engine;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sqlproc.engine.impl.SqlMappingRule;
import org.sqlproc.engine.impl.SqlMetaStatement;

/**
 * Helper class for the parsing of META SQL statements and Mapping rules located in properties file.
 * 
 * The purpose of this class is to load and analyze (=parse) META SQL statements and mapping rules located in the
 * properties repository. The standard properties repository is the external properties file with the name
 * <code>queries.properties</code>.
 * 
 * <p>
 * All the properties with the key <code>LIST_..._SQL</code> are parsed as META SQL statements using ANTLR based
 * grammar. <br>
 * All the properties with the key <code>LIST_..._FIELDS</code> are parsed as Mapping rules using ANTLR based grammar.<br>
 * A pair of META SQL statement and Mapping rule forms one named SQL Engine instance.<br>
 * All the properties with the key <code>SET_...</code> are taken as optional features used in the process of SQL Engine
 * instance construction.<br>
 * <p>
 * In the process of ANTLR based parsing different kinds of incorrect stuff can cause SqlEngineException to be raised.
 * 
 * <p>
 * To initialize this class, Spring DI configuration can be utilized, like the next one:<br>
 * 
 * <pre>
 * &lt;beans ...&gt;
 *   ...
 *   &lt;bean id="sqlQueries" class="org.springframework.beans.factory.config.PropertiesFactoryBean"&gt;
 *     &lt;property name="location"&gt;
 *       &lt;value>classpath:queries.properties&lt;/value&gt;
 *     &lt;/property&gt;
 *   &lt;/bean&gt;
 *   &lt;bean id="sqlLoader" class="org.sqlproc.engine.SqlEngineLoader"&gt;
 *     &lt;constructor-arg ref="sqlQueries" /&gt;
 *   &lt;/bean&gt;
 * &lt;/beans&gt;
 * </pre>
 * 
 * and use the next code to obtain instance of SQL Engine
 * 
 * <pre>
 * SqlEngine sqlEngine = sqlLoader.getSqlEngine(&quot;ALL&quot;);
 * </pre>
 * <p>
 * Another possibility is to utilize SqlPropertiesLoader.
 * 
 * <pre>
 * SqlPropertiesLoader loader = new SqlPropertiesLoader(&quot;queries.properties&quot;, this.getClass());
 * SqlEngineLoader sqlLoader = new SqlEngineLoader(loader.getProperties());
 * SqlEngine sqlEngine = sqlLoader.getSqlEngine(&quot;ALL&quot;);
 * </pre>
 * 
 * <p>
 * For more info please see the Reference Guide or <a
 * href="http://code.google.com/p/sql-processor/w/list">tutorials</a>.
 * 
 * @author <a href="mailto:Vladimir.Hudec@gmail.com">Vladimir Hudec</a>
 */
public class SqlEngineLoader {

    /**
     * The internal slf4j logger.
     */
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private static final String SET_PREFIX = "SET_";
    private static final int lSET_PREFIX = SET_PREFIX.length();
    private static final String FIELDS_REFERENCE = "#";
    private static final int lFIELDS_REFERENCE = FIELDS_REFERENCE.length();
    private static final String QUERY_PREFIX = "QRY_";
    private static final int lQUERY_PREFIX = QUERY_PREFIX.length();
    private static final String INPUT_MAPPING_PREFIX = "IN_";
    private static final int lINPUT_MAPPING_PREFIX = INPUT_MAPPING_PREFIX.length();
    private static final String OUTPUT_MAPPING_PREFIX = "OUT_";
    private static final int lOUTPUT_MAPPING_PREFIX = OUTPUT_MAPPING_PREFIX.length();
    private static final String CRUD_PREFIX = "CRUD_";
    private static final int lCRUD_PREFIX = CRUD_PREFIX.length();

    /**
     * The collection of named SQL Engine (the primary SQL Processor class) instances.
     */
    private Map<String, SqlEngine> engines = new HashMap<String, SqlEngine>();
    /**
     * The collection of named META SQL query statements
     */
    private Map<String, String> sqls = new HashMap<String, String>();
    /**
     * The collection of named META SQL CRUD statements
     */
    private Map<String, String> cruds = new HashMap<String, String>();
    /**
     * The collection of named Mapping rules
     */
    private Map<String, String> fields = new HashMap<String, String>();
    /**
     * The collection of SQL Processor optional features
     */
    private Map<String, Object> features = new HashMap<String, Object>();

    /**
     * Creates a new instance of SqlEngineLoader from the properties repository (which is in fact a collection of META
     * SQL statements, Mapping rules and Optional features. During the instance construction all the statements are
     * parsed and the collection of named SQL Engine instances are established. Later these instances are used for the
     * SQL queries invocation.
     * 
     * @param props
     *            The collection of META SQL statements, Mapping rules and Optional features
     * @throws SqlEngineException
     *             Mainly in the case the provided statements or rules are not compliant with the ANTLR based grammar.
     */
    public SqlEngineLoader(Properties props) throws SqlEngineException {
        this(props, null, null, (String[]) null);
    }

    /**
     * Creates a new instance of SqlEngineLoader from the properties repository (which is in fact a collection of META
     * SQL statements, Mapping rules and Optional features. During the instance construction all the statements are
     * parsed and the collection of named SQL Engine instances are established. Later these instances are used for the
     * SQL queries invocation.
     * 
     * @param props
     *            The collection of META SQL statements, Mapping rules and Optional features
     * @param filter
     *            The properties name prefix to filter META SQL statements, Mapping rules and Optional features.
     * @throws SqlEngineException
     *             Mainly in the case the provided statements or rules are not compliant with the ANTLR based grammar.
     */
    public SqlEngineLoader(Properties props, String filter) throws SqlEngineException {
        this(props, filter, null, (String[]) null);
    }

    /**
     * Creates a new instance of SqlEngineLoader from the properties repository (which is in fact a collection of META
     * SQL statements, Mapping rules and Optional features. During the instance construction all the statements are
     * parsed and the collection of named SQL Engine instances are established. Later these instances are used for the
     * SQL queries invocation. Every instance of SQL Engined is accompanied with SQL Monitor for the runtime statistics
     * gathering. For the creation of these monitors the SQL Monitor Factory can be used.
     * 
     * @param props
     *            The collection of META SQL statements, Mapping rules and Optional features
     * @param filter
     *            The properties name prefix to filter META SQL statements, Mapping rules and Optional features.
     * @param monitorFactory
     *            The monitor factory used in the process of SQL Monitor instances creation.
     * @throws SqlEngineException
     *             Mainly in the case the provided statements or rules are not compliant with the ANTLR based grammar.
     */
    public SqlEngineLoader(Properties props, String filter, SqlMonitorFactory monitorFactory) throws SqlEngineException {
        this(props, filter, monitorFactory, (String[]) null);
    }

    /**
     * Creates a new instance of SqlEngineLoader from the properties repository (which is in fact a collection of META
     * SQL statements, Mapping rules and Optional features. During the instance construction all the statements are
     * parsed and the collection of named SQL Engine instances are established. Later these instances are used for the
     * SQL queries invocation. Every instance of SQL Engined is accompanied with SQL Monitor for the runtime statistics
     * gathering. For the creation of these monitors the SQL Monitor Factory can be used.
     * 
     * @param props
     *            The collection of META SQL statements, Mapping rules and Optional features
     * @param filter
     *            The properties name prefix to filter META SQL statements, Mapping rules and Optional features.
     * @param monitorFactory
     *            The monitor factory used in the process of SQL Monitor instances creation.
     * @param selectQueries
     *            Only statements and rules with the names in this set are picked up from the properties repository.
     * @throws SqlEngineException
     *             Mainly in the case the provided statements or rules are not compliant with the ANTLR based grammar.
     */
    public SqlEngineLoader(Properties props, String filter, SqlMonitorFactory monitorFactory, String... selectQueries)
            throws SqlEngineException {
        if (logger.isTraceEnabled()) {
            logger.trace(">> SqlEngineLoader, props=" + props + ", monitorFactory=" + monitorFactory + ", filter="
                    + filter + ", selectQueries=" + selectQueries);
        }

        try {
            Set<String> setSelectQueries = (selectQueries != null) ? new HashSet<String>(Arrays.asList(selectQueries))
                    : null;

            String filterPrefix = (filter != null) ? filter.toUpperCase() : null;
            if (filterPrefix != null && !filterPrefix.endsWith("_"))
                filterPrefix = filterPrefix + "_";
            if (filterPrefix != null
                    && (filterPrefix.equals(QUERY_PREFIX) || filterPrefix.equals(CRUD_PREFIX)
                            || filterPrefix.equals(OUTPUT_MAPPING_PREFIX) || filterPrefix.equals(INPUT_MAPPING_PREFIX) || filterPrefix
                            .equals(SET_PREFIX)))
                filterPrefix = null;
            int filterPrefixLength = (filterPrefix != null) ? filterPrefix.length() : 0;

            StringBuilder errors = new StringBuilder();

            for (Entry<Object, Object> entry : props.entrySet()) {
                String key = ((String) entry.getKey()).toUpperCase();
                String value = (String) entry.getValue();
                String name = null;

                if (filterPrefix != null && key.startsWith(filterPrefix)) {
                    key = key.substring(filterPrefixLength);
                }

                if (key.startsWith(QUERY_PREFIX)) {
                    name = key.substring(lQUERY_PREFIX);
                    if (setSelectQueries == null || setSelectQueries.contains(name)) {
                        if (sqls.containsKey(name))
                            errors.append("Duplicate QRY: ").append(key).append("\n");
                        else
                            sqls.put(name, value);
                    }
                } else if (key.startsWith(CRUD_PREFIX)) {
                    name = key.substring(lCRUD_PREFIX);
                    if (setSelectQueries == null || setSelectQueries.contains(name)) {
                        if (cruds.containsKey(name))
                            errors.append("Duplicate CRUD: ").append(key).append("\n");
                        else
                            cruds.put(name, value);
                    }
                } else if (key.startsWith(OUTPUT_MAPPING_PREFIX)) {
                    name = key.substring(lOUTPUT_MAPPING_PREFIX);
                    if (setSelectQueries == null || setSelectQueries.contains(name)) {
                        if (fields.containsKey(name))
                            errors.append("Duplicate OUT: ").append(key).append("\n");
                        else
                            fields.put(name, value);
                    }
                } else if (key.startsWith(SET_PREFIX)) {
                    name = key.substring(lSET_PREFIX);
                    if ("true".equalsIgnoreCase(value))
                        features.put(name, Boolean.TRUE);
                    else if ("false".equalsIgnoreCase(value))
                        features.put(name, Boolean.FALSE);
                    else
                        features.put(name, value);
                } else {
                    // ignore the rest
                    continue;
                }
            }

            for (String name : fields.keySet()) {
                if (!sqls.containsKey(name))
                    errors.append("For the OUT/FIELDS there's no QRY: ").append(name).append("\n");
            }

            if (errors.length() > 0)
                throw new SqlEngineException(errors.toString());

            for (Field f : SqlFeature.class.getDeclaredFields()) {
                if (f.getName().startsWith("DEFAULT_")) {
                    String featureName = f.getName().substring(8);
                    if (features.get(featureName) == null) {
                        try {
                            features.put(featureName, f.get(null));
                        } catch (IllegalArgumentException e) {
                        } catch (IllegalAccessException e) {
                        }
                    }
                }
            }

            for (String name : sqls.keySet()) {
                SqlMetaStatement stmt = null;
                try {
                    stmt = SqlMetaStatement.getInstance(sqls.get(name));
                } catch (SqlEngineException see) {
                    errors.append(name + ":" + see.getMessage());
                    continue;
                }
                SqlMappingRule mapping = null;
                if (!stmt.isHasOutputMapping() && !fields.containsKey(name)) {
                    errors.append("For the QRY there's no OUT: ").append(name).append("\n");
                } else if (fields.containsKey(name)) {
                    try {
                        String sMapping = fields.get(name).trim();
                        if (sMapping.startsWith(FIELDS_REFERENCE)) {
                            String sRealMapping = props.getProperty(sMapping.substring(lFIELDS_REFERENCE).trim());
                            if (sRealMapping == null)
                                errors.append("For IN/OUT doesn't exist reference: ").append(name).append("->")
                                        .append(sMapping).append("\n");
                            else
                                mapping = SqlMappingRule.getInstance(sRealMapping);
                        } else if (!sMapping.isEmpty()) {
                            mapping = SqlMappingRule.getInstance(sMapping);
                        } else {
                            mapping = new SqlMappingRule();
                        }
                    } catch (SqlEngineException see) {
                        errors.append(see.getMessage());
                    }
                }
                SqlMonitor monitor = (monitorFactory != null) ? monitorFactory.getSqlMonitor(name, features) : null;
                if (stmt != null) {
                    engines.put(name, new SqlQueryEngine(name, stmt, mapping, monitor, features));
                }
            }

            for (String name : cruds.keySet()) {
                SqlMetaStatement stmt = null;
                try {
                    stmt = SqlMetaStatement.getInstance(cruds.get(name));
                } catch (SqlEngineException see) {
                    errors.append(see.getMessage());
                    continue;
                }
                SqlMonitor monitor = (monitorFactory != null) ? monitorFactory.getSqlMonitor(name, features) : null;
                if (stmt != null) {
                    engines.put(name, new SqlCrudEngine(name, stmt, monitor, features));
                }
            }

            if (errors.length() > 0)
                throw new SqlEngineException(errors.toString());
        } finally {
            if (logger.isDebugEnabled()) {
                logger.debug("<< SqlEngineLoader, engines=" + engines + ", sqls=" + sqls + ", cruds=" + cruds
                        + ", fields=" + fields + ", features=" + features);
            }
        }
    }

    /**
     * Returns the collection of names of all initialized/constructed SQL Engine instances.
     * 
     * @return Collection of all initialized SQL Engine instances' names
     */
    public Collection<String> getNames() {
        return engines.keySet();
    }

    /**
     * Returns the named META SQL statement.
     * 
     * @param name
     *            the name of the required META SQL statement
     * @return the META SQL statement
     */
    public String getMetaSql(String name) {
        return sqls.get(name);
    }

    /**
     * Returns the named Mapping rule.
     * 
     * @param name
     *            the name of the required Mapping rule
     * @return the Mapping rule
     */
    public String getMappingRule(String name) {
        return fields.get(name);
    }

    /**
     * Returns the named SQL Query Engine instance (the primary SQL Processor class).
     * 
     * @param name
     *            the name of the required SQL Query Engine instance
     * @return the SQL Engine instance
     */
    @Deprecated
    public SqlQueryEngine getSqlEngine(String name) {
        return getQueryEngine(name);
    }

    /**
     * Returns the named SQL Query Engine instance (the primary SQL Processor class).
     * 
     * @param name
     *            the name of the required SQL Query Engine instance
     * @return the SQL Engine instance
     */
    public SqlQueryEngine getQueryEngine(String name) {
        Object o = engines.get(name);
        if (o != null && o instanceof SqlQueryEngine)
            return (SqlQueryEngine) o;
        return null;
    }

    /**
     * Returns the named SQL CRUD Engine instance (the primary SQL Processor class).
     * 
     * @param name
     *            the name of the required SQL CRUD Engine instance
     * @return the SQL Engine instance
     */
    public SqlCrudEngine getCrudEngine(String name) {
        Object o = engines.get(name);
        if (o != null && o instanceof SqlCrudEngine)
            return (SqlCrudEngine) o;
        return null;
    }

    /**
     * Returns the SQL Monitor instance devoted to the named SQL Engine instance.
     * 
     * @param name
     *            the name of the SQL Engine instance
     * @return the SQL Monitor instance
     */
    public SqlMonitor getSqlMonitor(String name) {
        return getSqlEngine(name).getMonitor();
    }
}
